<?php
// based on http://buzzword.org.uk/2009/easypub/easypub.php.txt
// LGPL
class ConNeg
{
    public static function negotiate ($offers, $accept=null, $death=false)
    {
        if (!isset($accept))
        {
            $accept = $_SERVER['HTTP_ACCEPT'];
            header('Vary: Accept');
        }
        
        $a_parsed = self::parse_accept($accept);
        $o_parsed = self::parse_accept($offers);
        
        $best = self::choose_offer($o_parsed, $a_parsed);
        
        if (!isset($best))
        {
            if (!$death)
                return self::offer_serialise($o_parsed[0]);
            
            header('HTTP/1.1 406 Not Acceptable');
            header('Content-Type: text/plain; charset=utf-8');
            print "Acceptable types would have been:\n\n";
            foreach ($o_parsed as $o)
                print self::offer_serialise($o) . "\n";
            exit;
        }
        
        return self::offer_serialise($best);
    }

    # logically a constant, but a function is easier to comment.
    public static function STANDARD_OFFERS()
    {
        return 'application/xhtml+xml; charset=utf-8; x-serialisation=html, '   # XHTML+RDFa
            .'text/html; charset=utf-8; q=0.9; x-serialisation=html, '              #     Equivalent to above (for now)
            .'application/rdf+xml; x-serialisation=xml, '                               # RDF/XML
            .'text/rdf; charset=utf-8; x-serialisation=xml, '                           #     Alias for above
            .'application/xml; q=0.9; x-serialisation=xml, '                            #     Equivalent to above (for now)
            .'text/xml; charset=utf-8; q=0.9; x-serialisation=xml, '                    #     Equivalent to above (for now)
            .'application/rss+xml; x-serialisation=rss, '                               # RSS 1.0 compatible RDF/XML
            .'application/turtle; x-serialisation=turtle, '                             # Turtle
            .'application/x-turtle; x-serialisation=turtle, '                           #     Alias for above
            .'text/turtle; charset=utf-8; x-serialisation=turtle, '                 #     Alias for above
            .'text/n3; charset=utf-8; q=0.9; x-serialisation=turtle, '              #     Equivalent to above (for now)
            .'text/rdf+n3; charset=utf-8; q=0.9; x-serialisation=turtle, '          #         Alias for above
            .'application/json; x-serialisation=json, '                                 # JSON
            .'application/x-json; x-serialisation=json, '                               #     Alias for above
            .'application/ecmascript; x-serialisation=js, '                             # Javascript
            .'application/javascript; x-serialisation=js, '                             #     Alias for above
            .'text/ecmascript; charset=utf-8; x-serialisation=js, '                 #     Alias for above
            .'text/javascript; charset=utf-8; x-serialisation=js, '                 #     Alias for above
            .'text/plain; charset=utf-8; x-serialisation=ntriples, '                    # N-Triples
            .'application/turtle; level=nt; x-serialisation=ntriples, '             #     Alias for above
            .'application/x-turtle; level=nt; x-serialisation=ntriples, '           #     Alias for above
            .'text/turtle; charset=utf-8; level=nt; x-serialisation=ntriples'       #     Alias for above           
            ; 
    }
    
    public static function serialisation ($fmt)
    {
        if (preg_match('/x-serialisation=([a-z]+)/i', $fmt, $matches))
        {
            return $matches[1];
        }

        return 'xml';
    }

    private static function offer_serialise ($o)
    {
        $rv = array($o['_type']);
        foreach ($o as $k => $v)
        {
            if ($k!='_type' && $k!='_position' && $k!='q')
                $rv[] = sprintf("%s=%s", $k, $v);
        }
        return implode('; ', $rv);
    }

    private static function parse_accept ($header)
    {
        $rv = array();
        $bits = preg_split('/\s*,\s*/', $header);
        for ($i=0; isset($bits[$i]); $i++)
        {
            $bit = trim($bits[$i]);
            
            $pieces = preg_split('/\s*;\s*/', $bit);
            $type   = strtolower(trim(array_shift($pieces)));
            $entry  = array('_position' => $i+1);
            
            foreach ($pieces as $piece)
            {
                list ($key, $val) = preg_split('/\s*\=\s*/', trim($piece));
                if (!isset($entry[ strtolower(trim($key)) ]))
                    $entry[ strtolower(trim($key)) ] = trim($val);
            }
            
            if (!isset($entry['q']) || $entry['q'] > 1.0)
                $entry['q'] = 1.0;
            
            $entry['_type'] = $type;
            $rv[] = $entry;
        }
        
        return $rv;
    }

    private static function choose_offer ($offers, $requests)
    {
        $offer_scores = array();
        
        foreach ($offers as $offer)
        {
            foreach ($requests as $request)
            {
                if (self::match_offer($offer, $request))
                {
                    $score = $offer;
                    $score['q'] *= $request['q'];
                    $score['_position'] *= $request['_position'];
                    
                    $offer_scores[] = $score;
                }
            }
        }
        
        usort($offer_scores, array(__CLASS__, 'score_sort'));
        
        return $offer_scores[0];
    }

    private static function score_sort ($a, $b)
    {
        if ((float)$a['q'] < (float)$b['q'])
            return 1;
        if ((float)$a['q'] > (float)$b['q'])
            return -1;
            
        if ($a['_position'] < $b['_position'])
            return -1;
        if ($a['_position'] > $b['_position'])
            return 1;
        
        return 0;
    }

    private static function match_offer ($o, $r)
    {
        # Content-Type - look for a mismatch
        list ($omaj, $omin) = explode('/', $o['_type']);
        list ($rmaj, $rmin) = explode('/', $r['_type']);
        if (!($omaj==$rmaj || $omaj=='*' || $rmaj=='*'))
            return false;
        if (!($omin==$rmin || $omin=='*' || $rmin=='*'))
            return false;
        
        # Content-Type Parameters - look for a mismatch
        foreach ($r as $rparam=>$rvalue)
        {
            if ($rparam != 'q' && $rparam != '_type' && $rparam != '_position')
            {
                if ($o[$rparam] != $rvalue)
                    return false;
                print "OK\n";
            }
        }
        
        # No mismatches.
        return true;
    }
}
?>
